// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: gekim

#pragma once

#include <iostream>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <vector>

#include <boost/date_time/posix_time/posix_time.hpp>

#include "coin2/base/file.h"
#include "coin2/exchange/base/symbology/derivative_util.h"
#include "coin2/exchange/base/symbology/futures_product.h"
#include "coin2/exchange/kucoin_futures/symbology/currency.h"

namespace coin2::exchange::kucoin_futures::symbology {

using KucoinFuturesCurrency = kucoin_futures::symbology::KucoinFuturesCurrency;

class KucoinFuturesProduct
    : public base::symbology::FuturesProduct<KucoinFuturesCurrency, KucoinFuturesProduct>,
      public base::symbology::ProductFromStrMixin<KucoinFuturesProduct> {
 public:
  using FuturesProduct =
      base::symbology::FuturesProduct<KucoinFuturesCurrency, KucoinFuturesProduct>;
  using ContractType = base::symbology::ContractType;
  using MarketType = base::market::MarketType;

  KucoinFuturesProduct(
      const KucoinFuturesCurrency& base,
      const KucoinFuturesCurrency& quote,
      const std::string& native_product,
      const std::string& native_subscription_symbol,
      const ContractType contract_type,
      const int64_t expiry_timestamp,
      const int64_t resolve_timestamp)
      : FuturesProduct(
            base,
            quote,
            native_product,
            native_subscription_symbol,
            base::market::ExchangeType::Kucoin,
            contract_type,
            expiry_timestamp,
            resolve_timestamp) {}

 public:
  static std::unique_ptr<KucoinFuturesProduct> FromNativeStrImpl(
      const std::string& native_product,
      const int64_t resolve_timestamp) {
    using base::symbology::ContractType;
    using base::symbology::ExchangeType;
    using base::symbology::FindExpiryFriday;
    using base::symbology::IsDateStr;
    using base::symbology::StringToContractType;
    using base::symbology::ToDateStr;

    std::unique_ptr<KucoinFuturesCurrency> base;
    std::unique_ptr<KucoinFuturesCurrency> quote;
    std::string expiry_str = "PERPETUAL";

    if (native_product.back() == 'M') {
      size_t usdt_pos = native_product.rfind("USDT");
      if (usdt_pos != std::string::npos) {
        LOG(INFO) << native_product.substr(0, usdt_pos);
        base = KucoinFuturesCurrency::FromNativeStrNoThrow(
            native_product.substr(0, usdt_pos),
            resolve_timestamp);
        quote = KucoinFuturesCurrency::FromNativeStrNoThrow("USDT", resolve_timestamp);
      } else {
        size_t usdc_pos = native_product.rfind("USDC");
        if (usdc_pos != std::string::npos) {
          LOG(INFO) << native_product.substr(0, usdc_pos);
          base = KucoinFuturesCurrency::FromNativeStrNoThrow(
              native_product.substr(0, usdc_pos),
              resolve_timestamp);
          quote = KucoinFuturesCurrency::FromNativeStrNoThrow("USDC", resolve_timestamp);
        } else {
          size_t usd_pos = native_product.rfind("USD");
          LOG(INFO) << native_product.substr(0, usd_pos);
          base = KucoinFuturesCurrency::FromNativeStrNoThrow(
              native_product.substr(0, usd_pos),
              resolve_timestamp);
          quote = KucoinFuturesCurrency::FromNativeStrNoThrow("USD", resolve_timestamp);
        }
      }
    } else if (native_product.length() > 5) {
      std::string year_str = native_product.substr(native_product.length() - 2, 2);
      if (std::all_of(std::begin(year_str), std::end(year_str), ::isdigit)) {
        int month = CodeToMonth(native_product.substr(native_product.length() - 3, 1));
        if (month >= 0) {
          std::string quarter_date_str = ToDateStr(
              FindExpiryQuarter(resolve_timestamp));  // quarter_date_str for example 20220930
          std::string next_quarter_date_str = ToDateStr(FindExpiryNextQuarter(resolve_timestamp));
          if (quarter_date_str.size() == 8 || next_quarter_date_str.size() == 8) {
            int quarter_month = std::stoi(quarter_date_str.substr(4, 2));
            int next_quarter_month = std::stoi(next_quarter_date_str.substr(4, 2));
            if (quarter_month == month) {
              expiry_str = "QUARTER";
            }
            if (next_quarter_month == month) {
              expiry_str = "NEXT_QUARTER";
            }
          }

          base = KucoinFuturesCurrency::FromNativeStrNoThrow(
              native_product.substr(0, native_product.length() - 4),
              resolve_timestamp);
          quote = KucoinFuturesCurrency::FromNativeStrNoThrow("USD", resolve_timestamp);
        }
      }
    }

    if (!base || !quote) {
      return {};
    }

    std::string native_product_tmp;
    std::string native_subscription_symbol;
    ContractType contract_type;
    int64_t expiry_timestamp;
    bool success = CalculateParams(
        *base,
        *quote,
        expiry_str,
        resolve_timestamp,
        &native_product_tmp,
        &native_subscription_symbol,
        &contract_type,
        &expiry_timestamp);
    if (success) {
      DCHECK_EQ(native_product_tmp, native_product);
      return std::make_unique<KucoinFuturesProduct>(
          *base,
          *quote,
          native_product,
          native_subscription_symbol,
          contract_type,
          expiry_timestamp,
          resolve_timestamp);
    } else {
      return {};
    }
  }

  static char MonthToCode(int month) {
    const char* month_code_arr = "FGHJKMNQUVXZ";
    return month_code_arr[month - 1];
  }

  static int CodeToMonth(std::string month_str) {
    const std::string month_code_arr = "FGHJKMNQUVXZ";
    size_t pos = month_code_arr.find(month_str);
    if (pos == std::string::npos) {
      return -1;
    }
    return pos + 1;
  }

  static void GetMonthYearFromTimestamp(const int64_t& timestamp, int* year, int* month) {
    using namespace boost::posix_time;

    const auto dt = from_time_t(::ceil(timestamp / 1e9));
    *year = static_cast<int>(dt.date().year());
    *month = static_cast<int>(dt.date().month());
  }

  static bool CalculateParams(
      const KucoinFuturesCurrency& base,
      const KucoinFuturesCurrency& quote,
      const std::string& expiry_str,
      const int64_t resolve_timestamp,
      std::string* native_product,
      std::string* native_subscription_symbol,
      ContractType* contract_type,
      int64_t* expiry_timestamp) {
    using base::symbology::ContractType;
    using base::symbology::StringToContractType;
    using base::symbology::ToDateStr;

    if (std::all_of(std::begin(expiry_str), std::end(expiry_str), ::isdigit)) {
      bool found = false;

      if (!found) {
        // try quarter
        auto timestamp = FindExpiryQuarter(resolve_timestamp);
        if (expiry_str == ToDateStr(timestamp)) {
          *contract_type = ContractType::QUARTER;
          *expiry_timestamp = timestamp;
          found = true;
        }
      }

      if (!found) {
        // try next quarter
        auto timestamp = FindExpiryNextQuarter(resolve_timestamp);
        if (expiry_str == ToDateStr(timestamp)) {
          *contract_type = ContractType::NEXT_QUARTER;
          *expiry_timestamp = timestamp;
          found = true;
        }
      }

      if (!found) {
        return false;
      }
    } else {
      auto type = StringToContractType(expiry_str);
      if (type) {
        *contract_type = type.value();
        if (*contract_type == ContractType::PERPETUAL) {
          *expiry_timestamp = FuturesProduct::PERPETUAL_TIMESTAMP;
        } else if (*contract_type == ContractType::WEEK) {
          NOTREACHED();
        } else if (*contract_type == ContractType::QUARTER) {
          *expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
        } else if (*contract_type == ContractType::NEXT_QUARTER) {
          *expiry_timestamp = FindExpiryNextQuarter(resolve_timestamp);
        } else {
          NOTREACHED();
        }
      } else {
        return false;
      }
    }

    std::stringstream ss;
    if (*contract_type == ContractType::PERPETUAL) {
      ss << base.native_currency() << quote.native_currency() << "M";
    } else {
      using namespace boost::posix_time;

      auto expiry_date = from_time_t(::ceil(*expiry_timestamp / 1e9)).date();
      ss << base.native_currency() << "M" << MonthToCode(expiry_date.month())
         << std::to_string(expiry_date.year() % 100);
    }
    *native_product = ss.str();
    *native_subscription_symbol = *native_product;
    return true;
  }

  static int64_t FindExpiryQuarter(const int64_t resolve_timestamp) {
    int current_year;
    int current_month;
    GetMonthYearFromTimestamp(resolve_timestamp, &current_year, &current_month);
    auto expiry_month = current_month + (12 - current_month) % 3;

    using namespace boost::posix_time;
    using namespace boost::gregorian;
    using namespace coin2::exchange::base::symbology;

    auto possible_expiry_date =
        last_day_of_the_week_in_month(Friday, expiry_month).get_date(current_year);
    const auto td = microseconds(expiry_time_of_day_nanosec / 1000);
    int64_t possible_expiry_timestamp = DatetimeToTimestamp(ptime(possible_expiry_date, td));
    if (resolve_timestamp < possible_expiry_timestamp) {
      return possible_expiry_timestamp;
    } else {
      return FindExpiryQuarter(possible_expiry_timestamp + 75 * 24 * 3600 * 1e9);
    }
  }

  static int64_t FindExpiryNextQuarter(const int64_t resolve_timestamp) {
    int64_t quarter_expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
    return FindExpiryQuarter(quarter_expiry_timestamp + 75 * 24 * 3600 * 1e9);
  }

  static const int64_t expiry_time_of_day_nanosec = 8 * 3600 * 1e9;
};

}  // namespace coin2::exchange::kucoin_futures::symbology
